/*  -*- mode: c++; coding: utf-8; c-file-style: "stroustrup"; -*-

    Copyright 2008 Asier Aguirre <asier.aguirre@gmail.com>
    This file is part of Intromit.

    Intromit is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Intromit is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Intromit.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pymetadata_iface.hh"
#include "pymetadata_container.hh"
#include "pymetadata.hh"
#include "pymetadata_global.hh"
#include "pymetadata_vector.hh"
#include "gendata_common.hh"
#include "file_mng.hh"
#include "string.hh"
#include "util.hh"

#include <dlfcn.h>
#include <libgen.h>

// DEFINES
#define MODULE_NAME           "test"
#define GLOBAL_VARS_MODULE "globals"
#define FUNCTIONS_MODULE "functions"
#define MAX_NAME                 512
#define MAX_ABSOLUTE_PATH      16384
#define OWN_ELF                     const_cast<char*>(STR(PREFIX)"/lib/intromit/backends/pyintromit.so")
#define OWN_ELF_NOT_INSTALLED1      const_cast<char*>("../src/backends/python/intromit/backends/.libs/pyintromit.so")
#define OWN_ELF_NOT_INSTALLED2      const_cast<char*>("src/backends/python/intromit/backends/.libs/pyintromit.so")

CLASS_INIT(pymetadata); // PYMETADATA CLASS

pymetadata::pymetadata()
{
    CLASS_CTOR(pymetadata);
}

pymetadata::~pymetadata()
{
    CLASS_DTOR(pymetadata);
}

PyObject *pymetadata::load(char *filename, char *gendata_filename, char *module_name_)
{
    // initial check
    if(!filename || !*filename) MSG_R0("an elf file name is required");
    if(!file_mng::regular_nf(filename)) MSG_R0("cannot stat %s file", filename);

    // open metadata file
    struct
    {
	const char *regex, *format;
    } md_file[]=
      {
	  {"(.*)", gendata_filename}, // provided metadata file
	  {"(.*)",                               "%s.gendata"},
	  {"(.*)\\.so$",                         "%s.gendata"},
	  {"(.*)\\.so\\.[0-9]$",                 "%s.gendata"},
	  {"(.*)\\.so\\.[0-9]\\.[0-9]\\.[0-9]$", "%s.gendata"},
	  {"(.*)\\.o",                           "%s.gendata"},
	  {"(.*)\\.a",                           "%s.gendata"},
	  {"(.*)\\.lo",                          "%s.gendata"},
	  {"(.*)\\.la",                          "%s.gendata"},
	  {NULL, NULL},
      };
    char *cpy=strdup(filename), *re=NULL;
    int i=0;
    int i_meta=-1;
    while(md_file[i].regex)
    {
	if(!md_file[i].format || !*md_file[i].format) {i++; continue;}
	if((re=string::regex((char*)md_file[i].regex, cpy, 1/*grp*/)) &&
	   file_mng::regular(md_file[i].format, re))
	{
	    string *str=new string;
	    char *file;
	    if((i_meta=load_metadata(file=str->format_volatile(md_file[i].format, re)))<0)
	    {
		delete str;
		MSG_R0("cannot load metadata file: %s", file);
	    }
	    delete str;
	    break;
	}
	if(!i && gendata_filename)
	{
	    MSG("warning: cannot stat providad metadata file");
	    MSG("         trying ELF file derived combinations");
	}
	strcpy(cpy, filename);
	i++;
    }
    if(!md_file[i].regex)
    {
	if(cpy) free(cpy);
	MSG_R0("cannot find metadata file for %s", filename);
    }

    // construct module name
    char *module_name;
    if(!module_name_ || !*module_name_)
    {
	re=basename(re);
	while(*re=='.') re++;
	if(strchr(re, '.')) *strchr(re, '.')=0;
	if(!strlen(re))
	{
	    MSG("warning: cannot get suitable module name, using %s", MODULE_NAME);
	    module_name=strdup(MODULE_NAME);
	}
	else
	    module_name=strdup(re);
    }
    else
	module_name=strdup(module_name_);
    if(cpy) free(cpy);

    // create module
    PyObject *module;
    if(!(module=PyImport_AddModule(module_name))) //PyModule_New(module_name)))
    {
	MSG("Error adding module: %s", module_name);
	free(module_name);
	return NULL;
    }
    free(module_name);
    Py_INCREF(module);

    // check if aready loaded
    if(PyObject_HasAttrString(module, const_cast<char*>("__file__")))
    {
	// object already created
	PyObject *file=PyObject_GetAttrString(module, const_cast<char*>("__file__"));
	if(memcmp(PyString_AsString(file), filename, strlen(filename)))
	    MSG("Error: same python module name, but different C/C++ object");
	return module;
    }

    // get metadata
    pymetadata_global::metadata *meta=pymdg.get_meta(i_meta);

    // convert C++ names to python (including namespaces, class names, ...)
    int position=0;
    while(position<meta->n_data)
	if(!convert_names_to_python(position, meta->data, meta->doc))
	    MSG_R0("error converting to python names");

    // open elf if necesary
    void *dl;
    int i_dl_handle;
    if((i_dl_handle=pymdg.dl_handle_exists(filename))<0)
    {
	if(!strcmp(filename, OWN_ELF))
	{
	    // this is an special case, pyintromit.so is this code,
	    // so it's not necesary to duplicate it
	    if(!(dl=dlopen(NULL, RTLD_LAZY))) MSG_R0("dlopen: %s\n", dlerror());

	    // test it
	    if(!dlsym(dl, "pymdg"))
	    {
		// is not working, open file anyway
		dlclose(dl);
		dl=NULL;
	    }
	}
	else dl=NULL;

	if(!dl && !(dl=dlopen(filename, RTLD_LAZY))) MSG_R0("dlopen: %s\n", dlerror());
	pymdg.add_dl(filename, dl);
    }
    else dl=pymdg.get_dl_handle(i_dl_handle)->dl;

    // get n_types at this point to know latter witch are newer
    int n_type_init=pymdg.get_n_type();

    // find classes/structs/unions
    position=0;
    while(position<meta->n_data)
	if(!generate_class(position, meta->data, meta->doc, dl, module))
	    MSG_R0("error generating python metadata structures\n");

    // get the types ready
    for(i=n_type_init; i<pymdg.get_n_type(); i++)
	if(!pymdg.get_type(i)->ready())
	    MSG("cannot get ready type %s to context", pymdg.get_type(i)->get_name());

    // add classes/structs to python context (also perform python inheritance)
    for(i=n_type_init; i<pymdg.get_n_type(); i++)
	if(!pymdg.get_type(i)->add(module))
	    MSG("cannot add type %s to context", pymdg.get_type(i)->get_name());

    // generate rest of metadata
    position=0;
    while(position<meta->n_data)
	if(!generate_postready(position, meta->data, meta->doc, dl, module))
	    MSG_R0("error generating python metadata structures\n");

    // update refcnt
    for(i=n_type_init; i<pymdg.get_n_type(); i++)
	pymdg.get_type(i)->update_refcnt_from_bases();

    // update methods
    for(i=n_type_init; i<pymdg.get_n_type(); i++)
	pymdg.get_type(i)->update_methods();

    // sort types by vtable
    pymdg.sort_type_by_vtable();

    return module;
}

int pymetadata::load_metadata(char *file)
{
    int *data, n_data, n_doc, dev=-1;
    char *doc;
    FILE *arch;
    if((arch=fopen(file, "r")))
    {
	if(fread(&n_data, sizeof(int), 1, arch)==1 && n_data>=0)
	{
	    data=(int*)malloc(n_data*sizeof(int));
	    if(fread(data, sizeof(int), n_data, arch)==(unsigned)n_data &&
	       fread(&n_doc, sizeof(int), 1, arch)==1 && n_doc>=0)
	    {
		doc=(char*)malloc(n_doc);
		if(fread(doc, sizeof(char), n_doc, arch)==(unsigned)n_doc)
		{
		    if((dev=pymdg.meta_exists(data, n_data, doc, n_doc))<0)
		    {
			// does not exist -> add
			if((dev=pymdg.add_meta(data, n_data, doc, n_doc))<0)
			    MSG("Internal: add_meta");
		    }
		    else
		    {
			// already exist, free new doc and data
			free(doc);
			free(data);
		    }
		}
		else MSG("cannot read doc from %s: %s", file, strerror(errno));
	    }
	    else MSG("cannot read data and n_doc from %s: %s", file, strerror(errno));
	}
	else MSG("cannot read n_data from %s: %s", file, strerror(errno));
	fclose(arch);
    }
    else MSG("cannot open %s for reading: %s", file, strerror(errno));
    return dev;
}

int pymetadata::convert_names_to_python(int &i, int *data, char *doc)
{
    int type=data[i];
    if(type<0 || type>=GENDATA_LAST) MSG_R0("badly generated data: %d %d", i, type);

    // continue recursion
    int j=0, val;
    while((val=gendata_structs[type][++j])!=GENDATA_META_LAST)
    {
	if(val==GENDATA_GENDATA_GOTO_NEXT) continue;
	switch(val)
	{
	case GENDATA_GENDATA_VAR:
	    val=data[i++];
	    for(int k=0; k<val; k++) convert_names_to_python(i, data, doc);
	    continue;
	case GENDATA_NAME:
	    if(data[i]>=0)
	    {
		if(type==GENDATA_CLASS) pythonize_class_name(&doc[data[i]]);
		if(type==GENDATA_METHOD) pythonize_method_name(&doc[data[i]]);
	    }
	    break;
	case GENDATA_NAMESPACE:
	    if(data[i]>=0)
	    {
		if(type==GENDATA_CLASS) pythonize_class_namespace(&doc[data[i]]);
	    }
	    break;
	}
	i++;
    }
    return 1;
}

int pymetadata::generate_class(int &i, int *data, char *doc, void *dl, PyObject *module)
{
    int type=data[i];
    if(type<0 || type>=GENDATA_LAST) MSG_R0("badly generated data: %d %d", i, type);
    if(!module) MSG_R0("internal: no module");

    // analyse
    char *name;
    if((name=gendata_get_string(&data[i], doc, GENDATA_NAME)))
    {
	switch(type)
	{
	case GENDATA_CLASS: // classes
	    if(pymdg.add_type(new python_type_metadata(PyModule_GetName(module), &data[i], doc, dl))<0)
		MSG("cannot create python_type: %s", name);
	    break;
	default: // other types taken into account inside other metadata
	    break;
	}
    }

    // continue recursion
    int j=0, val;
    while((val=gendata_structs[type][++j])!=GENDATA_META_LAST)
    {
	if(val==GENDATA_GENDATA_GOTO_NEXT) continue;
	else if(val==GENDATA_GENDATA_VAR)
	{
	    val=data[i++];
	    for(int k=0; k<val; k++) generate_class(i, data, doc, dl, module);
	    continue;
	}
	i++;
    }
    return 1;
}

void *pymetadata::dlsym_also_try_mangled(void *dl, const char *symbol)
{
    void *addr;
    if((addr=dlsym(dl, symbol))) return addr;

    // try easy mangling
    int n_symbol=strlen(symbol);
    char *new_symbol=(char*)malloc(n_symbol+16);
    snprintf(new_symbol, n_symbol+16, "_ZL%d%s", n_symbol, symbol);
    addr=dlsym(dl, new_symbol);
    free(new_symbol);
    return addr;
}

int pymetadata::generate_postready(int &i, int *data, char *doc, void *dl, PyObject *module, int parent_is_class)
{
    int type=data[i];
    if(type<0 || type>=GENDATA_LAST) MSG_R0("badly generated data: %d %d", i, type);
    if(!module) MSG_R0("internal: no module");

    // analyse
    char *name;
    if((name=gendata_get_string(&data[i], doc, GENDATA_NAME)))
    {
	char *namespc=gendata_get_string(&data[i], doc, GENDATA_NAMESPACE);
	PyObject *tmp_mod;

	switch(type)
	{
	case GENDATA_ENUM: // enums
	    if(!python_container::route(module, namespc, name))
	    {
		if(!generate_enum(&data[i], doc, name, namespc, module))
		    MSG("cannot create python enum: %s", name);
	    }
	    break;
	case GENDATA_VARIABLE: // variables
	    if(namespc)
	    {
		// variable can be added directly
		if(parent_is_class) ; // do nothing, this is a static member maganed by the class
		else
		{
		    if(!(tmp_mod=python_container::route(module, namespc, NULL)))
			MSG_R0("internal: creating %s container (name=%s)", namespc, name);
		    else
		    {
			char *linkage; void *addr;
			if((linkage=gendata_get_string(&data[i], doc, GENDATA_LINKAGE)) &&
			   (addr=dlsym(dl, linkage)))
			{
			    python_container *container=(python_container *)tmp_mod;
			    container->set_pytype_doc(doc);
			    container->add_global_variable(name, &data[i], addr);
			}
			else
			    MSG("warning: cannot get address of %s::%s global variable", namespc, name);
		    }
		}
	    }
	    else
	    {
		// variable is added to special module "globals"
		if(!(tmp_mod=python_container::route(module, namespc,
						     const_cast<char*>(GLOBAL_VARS_MODULE),
						     1/*completing_allowed*/,
						     1/*name_is_container*/)))
		    MSG_R0("internal: creating %s container", GLOBAL_VARS_MODULE);
		else
		{
		    void *addr;
		    if((addr=dlsym_also_try_mangled(dl, name)))
		    {
			python_container *container=(python_container *)tmp_mod;
			container->set_pytype_doc(doc);
			container->add_global_variable(name, &data[i], addr);
		    }
		    else
			// static variables are not accessible by means of dlsym
			//MSG("warning: cannot get address of %s global variable", name);
			;
		}
	    }
	    break;
	case GENDATA_SUBROUTINE: // functions (not methods)
	    if(!(tmp_mod=python_container::route(module, namespc,
						 const_cast<char*>(FUNCTIONS_MODULE),
						 1/*completing_allowed*/,
						 1/*name_is_container*/)))
		MSG_R0("internal: creating %s container", FUNCTIONS_MODULE);
	    else
	    {
		python_container *container=(python_container *)tmp_mod;
		container->set_pytype_doc(doc);
		container->add_function(name, &data[i], dl);
	    }
	    break;
	default: // other types taken into account inside other metadata
	    break;
	}
    }

    // continue recursion
    int j=0, val;
    while((val=gendata_structs[type][++j])!=GENDATA_META_LAST)
    {
	if(val==GENDATA_GENDATA_GOTO_NEXT) continue;
	else if(val==GENDATA_GENDATA_VAR)
	{
	    val=data[i++];
	    for(int k=0; k<val; k++)
		generate_postready(i, data, doc, dl, module, (type==GENDATA_CLASS));
	    continue;
	}
	i++;
    }
    return 1;
}

int pymetadata::generate_enum(int *data, char *doc, char *name, char *namespc, PyObject *module)
{
    int n=gendata_get_data(data, GENDATA_GENDATA_VAR);
    data+=gendata_get_position(data, GENDATA_GENDATA_VAR)+1;

    if(!(module=python_container::route(module, namespc, NULL/*name*/, 1/*completing_allowed*/)))
	ERROR_R0("cannot create route");

    for(int i=0; i<n; i++)
    {
	char *name;
	int value;
	PyObject *pyvalue;

	// add to class container
	if(PyObject_SetAttrString(module,
				  name=gendata_get_string(data, doc, GENDATA_NAME),
				  pyvalue=PyLong_FromLong(value=gendata_get_data(data, GENDATA_CONST_VALUE))
	       )<0) MSG("cannot add constant to class object: %s=%d", name, value);

	Py_DECREF(pyvalue); // from new reference to borrow reference

	data=gendata_traverse(data);
    }
    return 1;
}

void pymetadata::pythonize_class_name(char *n)
{
    //ERROR("class name: %s", n);

    // special simplifications (TODO: be more rigurous, please)
    char *sep;
    const char *vector="vector<";
    if(!strncmp(n, vector, strlen(vector)) && (sep=strchr(n, ','))) sep[1]=0;

    for(int i=0; n[i]; i++)
	if(!isalnum(n[i])) n[i]='_';

    //ERROR("class name: %s  <-", n);
}

void pymetadata::pythonize_class_namespace(char *n)
{
    //ERROR("class namespace: %s", n);

    // substitude :: by .
    char *sep=n;
    int nn=strlen(n);
    while((sep=string::search(sep, (char*)"::", (char*)"<>")))
    {
	// namespace separator
	*sep='.';
	memmove(sep+1, sep+2, nn-(int)(sep-n)-1);
    }

    for(int i=0; n[i]; i++)
	if(!isalnum(n[i]) && n[i]!='.') n[i]='_';

    //ERROR("class namespace: %s  <-", n);
}

void pymetadata::pythonize_method_name(char *n)
{
#if 0
    ERROR("method name: %s", n);
    for(int i=0; n[i]; i++)
	if(!isalnum(n[i])) n[i]='_';
    ERROR("method name: %s  <-", n);
#endif
}

PyObject *pymetadata::pyload(PyObject *self, PyObject *args)
{
    char *filename, *gendata=NULL, *module=NULL;
    PyObject *pymodule;
    if(!PyArg_ParseTuple(args, const_cast<char*>("s|ss"), &filename, &gendata, &module)) return NULL;

    // check filename
    if(!filename || !*filename)
    {
	PyErr_SetString(PyExc_RuntimeError, "ERROR: bad filename");
	return NULL;
    }

    // make absolute path
    char *filename_abs=NULL;
    if(*filename!='/')
    {
	filename_abs=(char*)malloc(MAX_ABSOLUTE_PATH);
	if(!getcwd(filename_abs, MAX_ABSOLUTE_PATH))
	{
	    PyErr_SetString(PyExc_RuntimeError, "ERROR: getcwd");
	    return NULL;
	}
	int n_filename=strlen(filename_abs);
	snprintf(filename_abs+n_filename, MAX_ABSOLUTE_PATH-n_filename, "/%s", filename);
	filename=filename_abs;
    }

    // dynamically create python module from its metadata
    if(!(pymodule=load(filename, gendata, module)))
    {
	if(filename_abs) free(filename_abs);
	PyErr_SetString(PyExc_RuntimeError, "ERROR: load");
	return NULL;
    }

    // functions
    PyObject *tmp_mod;
    PyMethodDef *functions=NULL;
    if((tmp_mod=python_container::route(pymodule, NULL, const_cast<char*>(FUNCTIONS_MODULE))))
    {
	python_container *container=(python_container *)tmp_mod;
	functions=container->get_functions();
    }

    // filename attribute
    PyObject_SetAttrString(pymodule, const_cast<char*>("__file__"), PyString_FromFormat("%s.intromit", filename));

    Py_InitModule(PyModule_GetName(pymodule), functions);
    if(filename_abs) free(filename_abs);
    return pymodule;
}

PyObject *pymetadata::pyevent_queue_remove(PyObject *self, PyObject *args)
{
    char *method_name=NULL;
    PyObject *object=NULL;
    if(!PyArg_ParseTuple(args, const_cast<char*>("|Os"), &object, &method_name)) return NULL;
    if(!pymdg.event_queue_remove((python_metadata *)object, method_name))
    {
	PyErr_SetString(PyExc_RuntimeError, "ERROR: no such object/method");
	return NULL;
    }
    Py_RETURN_NONE;
}

PyMODINIT_FUNC initpyintromit(void)
{
    static PyMethodDef pyintromit_methods[]=
	{
	    {const_cast<char*>("load"), pymetadata::pyload, METH_VARARGS,
	     const_cast<char*>("load(ELF_filename[, metadata_filename[, module_name]])\n\nload C/C++ program/library")},
	    {const_cast<char*>("event_queue_remove"), pymetadata::pyevent_queue_remove, METH_VARARGS,
	     const_cast<char*>("event_queue_remove(object=None, method_name=None)\n\nRemoves objects/methods from event queue")},
	    {NULL, NULL, 0, NULL}
	};

    Py_InitModule(const_cast<char*>("pyintromit"), pyintromit_methods);

    // add python_vector class
    if(!pymetadata::load(OWN_ELF) &&
       !pymetadata::load(OWN_ELF_NOT_INSTALLED1) &&
       !pymetadata::load(OWN_ELF_NOT_INSTALLED2))
	ERROR("Cannot get metadata from: %s", OWN_ELF);
}
